Appearance
Java BigDecimal
在带有小数的数值计算(+
、-
、*
、/
)中,float
和 double
类型的计算结果往往不如人意。
java
System.out.println(1.1 + 0.1); // 1.2000000000000002
System.out.println(1.1 - 0.1); // 1.0
System.out.println(1.1 * 0.1); // 0.11000000000000001
System.out.println(1.1 / 0.1); // 11.0
这是因为现在的计算机主要采用的是754 - IEEE 浮点算术标准来表示浮点数,对小数的存储只能尽量做到精确,却做不到完全精确。
《Effective Java》这本书中对 float
和 double
的使用总结的很好:
float
和double
类型主要用于科学和工程计算。 它们执行二进制浮点运算,经过精心设计,可在很宽的范围内快速提供准确的近似值。 但是,它们不能提供准确的结果,不应在需要确切结果的地方使用。float
和double
类型特别不适合进行货币计算,因为不可能将 0.1(或任何其他 10 的负次方)精确地表示为float
或double
。
解决此问题的正确方法是使用 BigDecimal
,int
或 long
进行货币计算。也就是说,可以将小数转成 BigDecimal
,或将小数乘以一个固定的倍率转换为 int
或 long
,计算完成后再除以倍率转换回来。
BigDecimal 简介
BigDecimal
的全类名为 java.math.BigDecimal
。根据 Java API 文档说明,BigDecimal
的特点如下:
- 不可变的、任意精度的有符号小数
- 提供用于算术、数值范围处理、舍入、比较、哈希和格式转换的操作
- 完全控制舍入行为
BigDecimal 构造方法
BigDecimal
类提供了多个构造方法供我们实例化 BigDecimal
,常用的是下面几种:
public BigDecimal(int val)
将int
转换为BigDecimal
public BigDecimal(double val)
将double
转换为BigDecimal
(不建议)public BigDecimal(String val)
将String
转换为BigDecimal
java
// 实例化 BigDecimal
BigDecimal bigDecimal = new BigDecimal(10); // 将 int 转换为 BigDecimal
BigDecimal bigDecimal1 = new BigDecimal(10.0); // 将 double 转换为 BigDecimal(不建议)
BigDecimal bigDecimal2 = new BigDecimal("10.0"); // 将 String 转换为 BigDecimal
为什么不建议使用 BigDecimal(double val)
来实例化 BigDecimal
:
java
// 不建议使用 double 构造 BigDecimal
BigDecimal bigDecimal = new BigDecimal(0.1);
System.out.println(bigDecimal);
打印结果为:
0.1000000000000000055511151231257827021181583404541015625
可以看到,这比我们直接使用 double
类型还要糟糕。对此,Java API 文档做了注释:
这个构造函数的结果可能有些不可预测。您可能会假设在 Java 中写入新的
BigDecimal(0.1)
会创建一个完全等于 0.1 的BigDecimal(一个未缩放的值 1,刻度为 1)
,但是它实际上等于 0.1000000000000000055511151231257827021181583404541015625。这是因为 0.1 不能精确地表示为一个双精度数(或者说,任何有限长度的二进制分数)。因此,传递给构造函数的值并不完全等于 0.1,尽管看起来是这样。另一方面,字符串构造函数是完全可预测的:编写新的
BigDecimal("0.1")
将创建一个完全等于 0.1 的BigDecimal
,这与预期的一样。因此,通常建议优先使用字符串构造函数。当
double
必须用作BigDecimal
的源时,请注意此构造函数提供了精确的转换;与使用double.tostring(double)
方法将double
转换为字符串,然后使用BigDecimal(String)
构造函数得到的结果不同。要得到这个结果,使用静态valueOf(double)
方法。
也就是说,必须使用 double
创建一个 BigDecimal
时,可以使用 BigDecimal.valueOf(double)
静态方法生成 BigDecimal
对象:
java
// 使用 BigDecimal.valueOf(double) 静态方法生成 BigDecimal
BigDecimal bigDecimal = BigDecimal.valueOf(0.1);
System.out.println(bigDecimal); // 0.1
BigDecimal 算术运算(加减乘除)
BigDecimal
提供了 add
、subtract
、multiply
、divide
方法进行“加减乘除”运算操作。
java
// BigDecimal 加减乘除
BigDecimal x = new BigDecimal("1.1");
BigDecimal y = new BigDecimal("0.1");
System.out.println("x + y = " + x.add(y)); // x + y = 1.2
System.out.println("x - y = " + x.subtract(y)); // x - y = 1.0
System.out.println("x * y = " + x.multiply(y)); // x * y = 0.11
System.out.println("x / y = " + x.divide(y)); // x / y = 11
可以看到,这样的结果才是我们想要的。
注意
就像 String
一样,BigDecimal
也是不可变的类型。这意味着“加减乘除”的运算结果都会返回一个全新的 BigDecimal
。
BigDecimal 中的除法
对于 “10 / 3” 的结果,数学中可以表示为 “≈3.33” ,而在 Java 中会直接舍去小数部分,只取商。
java
System.out.println(10 / 3); // 3
想要保留小数部分,需要除数或被除数为小数形式:
java
System.out.println(10.0 / 3); // 3.3333333333333335
可是 float
和 double
运算结果的精度是无法预料的,那使用 BigDecimal
呢?
java
BigDecimal i = BigDecimal.valueOf(10);
BigDecimal j = BigDecimal.valueOf(3);
System.out.println("i / j = " + i.divide(j)); // java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
“10 / 3”的结果是一个无穷小数,不抛异常才怪(原来刚才计算“1.1 / 0.1”正常,完全是运气好 o(∩_∩)o 哈哈)。
BigDecimal divide(BigDecimal divisor)
方法的描述如下:
返回一个
BigDecimal
,其值为(this / divisor),其首选数值范围为(this.scale() - divisor.scale());如果不能表示精确的商(由于它是无限小数),则抛出ArithmeticException
异常。
为了解决除不尽的问题,需要使用 divide
的重载(overload)方法:
BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode)
方法参数含义为:
divisor - 除数
scale - 小数的数值范围(小数点后几位)
roundingMode - 舍入模式(八种舍入模式)
例如,要保留两位小数,且舍去(截断)后面的小数:
java
BigDecimal i = BigDecimal.valueOf(10);
BigDecimal j = BigDecimal.valueOf(3);
System.out.println("i / j = " + i.divide(j, 2, RoundingMode.DOWN)); // 3.33
BigDecimal 中的舍入模式
RoundingMode
类是一个枚举类,其中有八种枚举类型(UP
、DOWN
、CEILING
、FLOOR
、HALF_UP
、HALF_DOWN
、HALF_EVEN
、UNNECESSARY
),分别代表八种不同的舍入模式。
RoundingMode.UP
进一。以 0 为基准,舍入后的数字远离 0。
输入数字 | 远离 0 取整 |
---|---|
5.5 | 6 |
2.5 | 3 |
1.6 | 2 |
1.1 | 2 |
1.0 | 1 |
-1.0 | -1 |
-1.1 | -2 |
-1.6 | -2 |
-2.5 | -3 |
-5.5 | -6 |
总结:RoundingMode.UP
模式下,正数往大舍入,负数往小舍入。
RoundingMode.DOWN
截断。以 0 为基准,舍入后的数字靠近 0。
输入数字 | 靠近 0 取整 |
---|---|
5.5 | 5 |
2.5 | 2 |
1.6 | 1 |
1.1 | 1 |
1.0 | 1 |
-1.0 | -1 |
-1.1 | -1 |
-1.6 | -1 |
-2.5 | -2 |
-5.5 | -5 |
总结:RoundingMode.DOWN
模式下,正数往小舍入,负数往大舍入。
RoundingMode.CEILING
舍入后的数字靠近正无穷。
输入数字 | 靠近正无穷 |
---|---|
5.5 | 6 |
2.5 | 3 |
1.6 | 2 |
1.1 | 2 |
1.0 | 1 |
-1.0 | -1 |
-1.1 | -1 |
-1.6 | -1 |
-2.5 | -2 |
-5.5 | -5 |
总结:RoundingMode.CEILING
模式下,正数与负数都往大舍入。
RoundingMode.FLOOR
舍入后的数字靠近负无穷。
输入数字 | 靠近负无穷 |
---|---|
5.5 | 5 |
2.5 | 2 |
1.6 | 1 |
1.1 | 1 |
1.0 | 1 |
-1.0 | -1 |
-1.1 | -2 |
-1.6 | -2 |
-2.5 | -3 |
-5.5 | -6 |
总结:RoundingMode.FLOOR
模式下,正数与负数都往小舍入。
RoundingMode.HALF_UP
四舍五入。如果舍去位的值 >= 5,舍入模式同 RoundingMode.UP
;否则同 RoundingMode.DOWN
。
输入数字 | “四舍”靠近 0,“五入”远离 0 |
---|---|
5.5 | 6 |
2.5 | 3 |
1.6 | 2 |
1.1 | 1 |
1.0 | 1 |
-1.0 | -1 |
-1.1 | -1 |
-1.6 | -2 |
-2.5 | -3 |
-5.5 | -6 |
总结:RoundingMode.HALF_UP
模式下,“四舍”同 RoundingMode.DOWN
,“五入”同 RoundingMode.UP
。
RoundingMode.HALF_DOWN
五舍六入。如果舍去位的值 >= 6,舍入模式同 RoundingMode.UP
;否则同 RoundingMode.DOWN
。
输入数字 | “五舍”靠近 0,“六入”远离 0 |
---|---|
5.5 | 5 |
2.5 | 2 |
1.6 | 2 |
1.1 | 1 |
1.0 | 1 |
-1.0 | -1 |
-1.1 | -1 |
-1.6 | -2 |
-2.5 | -2 |
-5.5 | -5 |
总结:RoundingMode.HALF_DOWN
模式下,“五舍”同 RoundingMode.DOWN
,“六入”同 RoundingMode.UP
。
RoundingMode.HALF_EVEN
“银行家的舍入(Banker's Rounding)”模式。
- 如果舍去位的值 > 5,舍入模式同
RoundingMode.UP
; - 如果舍去位的值 < 5,舍入模式同
RoundingMode.DOWN
; - 如果舍去位的值 = 5 且 5 后不为空且非全 0 时,舍入模式同
RoundingMode.UP
; - 如果舍去位的值 = 5 且 5 后为空或全 0 时:如果前位数值为奇数,舍入模式同
RoundingMode.UP
;如果前位数值为偶数,舍入模式同RoundingMode.DOWN
。
输入数字 | “银行家的舍入” |
---|---|
5.5 | 6 |
2.5 | 2 |
1.6 | 2 |
1.1 | 1 |
1.0 | 1 |
-1.0 | -1 |
-1.1 | -1 |
-1.6 | -2 |
-2.5 | -2 |
-5.5 | -6 |
总结:RoundingMode.HALF_EVEN
模式下,四舍六入五考虑,五后非空就进一,五后为空看奇偶,五前为偶应舍去,五前为奇要进一。
RoundingMode.UNNECESSARY
断言所请求的操作有确切的结果,因此不需要舍入。如果在产生不精确结果的操作上指定了这种舍入模式,则会抛出 ArithmeticException
异常。
输入数字 | 不需要舍入 |
---|---|
5.5 | throw ArithmeticException |
2.5 | throw ArithmeticException |
1.6 | throw ArithmeticException |
1.1 | throw ArithmeticException |
1.0 | 1 |
-1.0 | -1 |
-1.1 | throw ArithmeticException |
-1.6 | throw ArithmeticException |
-2.5 | throw ArithmeticException |
-5.5 | throw ArithmeticException |
总结:RoundingMode.UNNECESSARY
模式下,必须保证结果是精确的。
BigDecimal 比较(compareTo 和 equals)
对于 float
、double
等基本的数值类型,可以通过布尔比较运算符(<
、==
、>
、>=
、!=
、<=
)比较大小及是否相等。而对于它们的包装类型(Float
、Double
等),由于 Java 没有重载对应的运算符,所以只能通过 compareTo
和 equals
方法来比较大小及是否相等。
同样,在 BigDecimal
中,也提供了这两个方法。
java
// compareTo 和 equals
BigDecimal x = new BigDecimal(10);
BigDecimal y = new BigDecimal(3);
System.out.println(x.compareTo(y)); // 1
System.out.println(x.equals(y)); // false
compareTo
的返回值是一个 int
,具体值只会是 -1、0、1 其中之一。
- 如果 x < y,则返回 -1
- 如果 x = y,则返回 0
- 如果 x > y,则返回 1
通过使用 compareTo
,我们可以自行组合不同的布尔运算,建议方法是:(x.compareTo(y) <op> 0)
,其中 <op>
是六个布尔比较运算符(<
、==
、>
、>=
、!=
、<=
)之一。
java
// 组合不同的布尔运算(<, ==, >, >=, !=, <=)
BigDecimal x = new BigDecimal(10);
BigDecimal y = new BigDecimal(3);
System.out.println("x < y = " + (x.compareTo(y) < 0)); // x < y = false
System.out.println("x == y = " + (x.compareTo(y) == 0)); // x == y = false
System.out.println("x > y = " + (x.compareTo(y) > 0)); // x > y = true
System.out.println("x >= y = " + (x.compareTo(y) >= 0)); // x >= y = true
System.out.println("x != y = " + (x.compareTo(y) != 0)); // x != y = true
System.out.println("x <= y = " + (x.compareTo(y) <= 0)); // x <= y = false
提示
与
compareTo
不同的是,equals
方法只在两个BigDecimal
对象在值和数值范围上相等时才认为它们相等(因此通过该方法进行比较时,2.0 不等于 2.00)。
Java API 文档中的描述并不完整,这句话只对通过字符串构造的 BigDecimal
对象生效。
java
// 通过字符串构造的 2.0 2.00
BigDecimal x = new BigDecimal("2.0");
BigDecimal y = new BigDecimal("2.00");
System.out.println(x.compareTo(y) == 0); // true
System.out.println(x.equals(y)); // false
// 通过数值构造的 2.0 2.00
BigDecimal xx = new BigDecimal(2.0);
BigDecimal yy = new BigDecimal(2.00);
System.out.println(xx.compareTo(yy) == 0); // true
System.out.println(xx.equals(yy)); // true